/*
 * Copyright (C) 2015 Daniel Uppenkamp
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.maurersmarbles.states;

import com.google.common.base.Preconditions;
import com.googlecode.maurersmarbles.Board;
import com.googlecode.maurersmarbles.Card;
import com.googlecode.maurersmarbles.CardsDisplay;
import com.googlecode.maurersmarbles.GameLogic;
import com.googlecode.maurersmarbles.GameState;
import com.googlecode.maurersmarbles.InputEvent;
import com.googlecode.maurersmarbles.Marble;
import com.googlecode.maurersmarbles.Play;
import com.googlecode.maurersmarbles.Player;
import com.googlecode.maurersmarbles.Point;
import com.googlecode.maurersmarbles.Rectangle;
import com.googlecode.maurersmarbles.Screen;
import com.googlecode.maurersmarbles.TouchEvent;
import com.googlecode.maurersmarbles.drawables.ButtonDrawable;
import com.googlecode.maurersmarbles.drawables.CardDrawable;
import com.googlecode.maurersmarbles.drawables.Drawable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Daniel Uppenkamp
 */
public class PlayerTurnState extends TableState {

    /**
     * The PlayerTurnState instance.
     */
    private static final PlayerTurnState INSTANCE = new PlayerTurnState();
    /**
     * The amount of time (in millis) to sleep when a user/player cannot play.
     */
    protected static final long SLEEP_TIME_PLAYER_CANNOT_PLAY = 1000L;
    /**
     * The amount of time (in millis) to sleep when the player is thinking.
     */
    protected static final long SLEEP_TIME_PLAYER_THINKING = 500L;
    /**
     * The text for the one button.
     */
    protected static final String BUTTON_TEXT_1 = "One";
    /**
     * The text for the two button.
     */
    protected static final String BUTTON_TEXT_2 = "Two";
    /**
     * The text for the three button.
     */
    protected static final String BUTTON_TEXT_3 = "Three";
    /**
     * The text for the four button.
     */
    protected static final String BUTTON_TEXT_4 = "Four";
    /**
     * The text for the five button.
     */
    protected static final String BUTTON_TEXT_5 = "Five";
    /**
     * The text for the six button.
     */
    protected static final String BUTTON_TEXT_6 = "Six";
    /**
     * The text for the seven button.
     */
    protected static final String BUTTON_TEXT_7 = "Seven";
    /**
     * The token in the status messages that is replaced with the player number.
     */
    protected static final String PLAYER_NUMBER_TOKEN = "#";
    /**
     * The status message to display when a player cannot play.
     */
    protected static final String STATUS_PLAYER_CANNOT_PLAY = "Player " + PLAYER_NUMBER_TOKEN + " Cannot Play";
    /**
     * The status message to display when a player is thinking.
     */
    protected static final String STATUS_PLAYER_THINKING = "Player " + PLAYER_NUMBER_TOKEN + " is Thinking";
    /**
     * The status message to display when the user cannot play.
     */
    protected static final String STATUS_USER_CANNOT_PLAY = "You Cannot Play";
    /**
     * The status message to display when it is the user's turn.
     */
    protected static final String STATUS_USER_TURN = "Your Turn";
    /**
     * Whether or not the split seven buttons should be drawn.
     */
    protected boolean drawSplitSevenButtons = false;
    /**
     * The card that is currently selected.
     */
    protected Card selectedCard = null;
    /**
     * The player number.
     */
    protected int playerNumber;
    /**
     * The value selected for a split seven.
     */
    protected int splitSevenValue = -1;
    /**
     * The map that contains the drawables.
     */
    protected Map<ButtonDrawable, Integer> buttonDrawables = new HashMap();
    /**
     * The marble that was previously selected.
     */
    protected Marble previousMarble = null;

    /**
     * Creates a new PlayerTurnState.
     */
    private PlayerTurnState() {

        // create and add the button drawables to the list of button drawables
        buttonDrawables.put(new ButtonDrawable(BUTTON_TEXT_1), 1);
        buttonDrawables.put(new ButtonDrawable(BUTTON_TEXT_2), 2);
        buttonDrawables.put(new ButtonDrawable(BUTTON_TEXT_3), 3);
        buttonDrawables.put(new ButtonDrawable(BUTTON_TEXT_4), 4);
        buttonDrawables.put(new ButtonDrawable(BUTTON_TEXT_5), 5);
        buttonDrawables.put(new ButtonDrawable(BUTTON_TEXT_6), 6);
        buttonDrawables.put(new ButtonDrawable(BUTTON_TEXT_7), 7);

    }

    /**
     * Returns the PlayerTurnState instance.
     *
     * @param playerNumber the player number
     * @return the PlayerTurnState instance
     */
    public static PlayerTurnState getInstance(int playerNumber) {
        INSTANCE.playerNumber = playerNumber;
        return INSTANCE;
    }

    /**
     * Returns the marble at the given screen x, y location, null if there is no
     * marble at that location.
     *
     * @param gameState the game state
     * @param x the x location on the screen
     * @param y the y location on the screen
     * @return the marble at the given screen x/y location, null if no marble
     */
    protected Marble getMarbleAtScreenXY(GameState gameState, int x, int y) {

        Preconditions.checkNotNull(gameState, "Null game state");
        Preconditions.checkNotNull(boardDrawable, "Null board drawable");

        Point gridCellPoint = boardDrawable.getGridCellAtScreenXY(x, y);
        if (gridCellPoint != null) {

            int index = Board.getIndexForGridCell(gridCellPoint);
            for (Player player : gameState.getPlayers()) {

                for (Marble marble : player.getMarbles()) {

                    if (marble.getBoardIndex() == index) {
                        return marble;
                    }

                }

            }

        }

        return null;

    }

    /**
     * Returns the rectangle for the given button.
     *
     * @param screen the screen
     * @param buttonDrawable the button drawable
     * @return the rectangle for the given button
     */
    protected Rectangle getRectangleForButton(Screen screen, ButtonDrawable buttonDrawable) {

        Preconditions.checkNotNull(screen, "Null screen");
        Preconditions.checkNotNull(boardDrawable, "Null board drawable");
        Preconditions.checkNotNull(buttonDrawables, "Null button drawables");

        // TODO Make these values constant
        float BOARD_WIDTH_TO_BUTTON_WIDTH = 0.75f;
        int NUMBER_OF_ROWS = 4;
        int padding = screen.getHeight() / 75;

        Rectangle boardRectangle = boardDrawable.getRectangle();

        // calculate the row/col to draw this drawable in
        int number = buttonDrawables.get(buttonDrawable);
        int row = (number - 1) / 2;
        int col = (number % 2 == 0) ? 1 : 0;

        // calculate the button width based on which row it is in
        int buttonWidth;
        int fullButtonWidth = (int) (boardRectangle.getWidth() * BOARD_WIDTH_TO_BUTTON_WIDTH);
        if (row == NUMBER_OF_ROWS - 1) {
            buttonWidth = fullButtonWidth;
        }
        else {
            buttonWidth = (fullButtonWidth / 2) - (padding / 2);
        }

        // calculate the button height using the standard ratio
        int buttonHeight = (int) (screen.getHeight() * ButtonDrawable.SCREEN_HEIGHT_TO_BUTTON_HEIGHT);

        double neededWidth = fullButtonWidth;
        double neededHeight = (buttonHeight * NUMBER_OF_ROWS) + (padding * (NUMBER_OF_ROWS - 1));

        double x = boardRectangle.getX() + (boardRectangle.getWidth() / 2.0) - (neededWidth / 2.0);
        double y = boardRectangle.getY() + (boardRectangle.getHeight() / 2.0) - (neededHeight / 2.0);

        int startX = (int) (x + (col * (buttonWidth + padding)));
        int startY = (int) (y + (row * (buttonHeight + padding)));

        return new Rectangle(startX, startY, buttonWidth, buttonHeight);

    }

    /**
     * Handles the pressed touch event based on the screen state and game state.
     *
     * @param screen the screen
     * @param gameState the game state
     * @param touchEvent the touch event
     * @return the state to transition to, or null to stay in the current state
     */
    protected State handleTouchEventPressed(Screen screen, GameState gameState, TouchEvent touchEvent) {

        Preconditions.checkNotNull(screen, "Null screen");
        Preconditions.checkNotNull(buttonDrawables, "Null button drawables");

        if (drawSplitSevenButtons) {
            for (ButtonDrawable buttonDrawable : buttonDrawables.keySet()) {
                Rectangle rectangle = buttonDrawable.getRectangle();
                if (rectangle != null && rectangle.contains(touchEvent.getX(), touchEvent.getY())) {
                    buttonDrawable.setPressed(true);
                    screen.draw(getDrawables(screen, gameState).values());
                    break;
                }
            }
        }

        return null;

    }

    /**
     * Handles the released touch event based on the screen state and game
     * state.
     *
     * @param screen the screen
     * @param gameState the game state
     * @param touchEvent the touch event
     * @return the state to transition to, or null to stay in the current state
     */
    protected State handleTouchEventReleased(Screen screen, GameState gameState, TouchEvent touchEvent) {

        Preconditions.checkNotNull(screen, "Null screen");
        Preconditions.checkNotNull(gameState, "Null game state");
        Preconditions.checkNotNull(buttonDrawables, "Null button drawables");
        Preconditions.checkNotNull(cardsDisplayMap, "Null cards display map");

        // check to see if the split seven buttons are drawn on the screen
        if (drawSplitSevenButtons) {

            // reset the split seven buttons to not pressed
            for (ButtonDrawable buttonDrawable : buttonDrawables.keySet()) {
                buttonDrawable.setPressed(false);
            }

            // check to see which button was pressed
            for (ButtonDrawable buttonDrawable : buttonDrawables.keySet()) {

                Rectangle rectangle = buttonDrawable.getRectangle();
                if (rectangle != null && rectangle.contains(touchEvent.getX(), touchEvent.getY())) {
                    splitSevenValue = buttonDrawables.get(buttonDrawable);
                    drawSplitSevenButtons = false;
                    break;
                }

            }

            /**
             * If the user selected the 7 button, we already have the marble to
             * move so we will drop down into the normal logic below to handle
             * moving the marble 7 spaces. If any other value was selected or
             * the user did not press a button, we will redraw to update the
             * screen and return.
             */
            if (splitSevenValue != 7) {
                screen.draw(getDrawables(screen, gameState).values());
                return null;
            }

        }

        CardsDisplay cardsDisplay = cardsDisplayMap.get(gameState.getUserPlayerNumber());
        if (cardsDisplay != null) {

            Card card = cardsDisplay.getCardAtScreenXY(touchEvent.getX(), touchEvent.getY());
            if (card != null) {

                // set the selected card
                selectedCard = card;

                // reset the previous marble and split 7 logic
                previousMarble = null;
                splitSevenValue = -1;

                screen.draw(getDrawables(screen, gameState).values());
                return null;

            }

        }

        if (selectedCard != null) {

            Marble marble = getMarbleAtScreenXY(gameState, touchEvent.getX(), touchEvent.getY());

            if (marble != null || splitSevenValue == 7) {

                Marble marble1 = marble;
                Marble marble2 = null;

                if (selectedCard.getId() == Card.CARD_ID_7 || selectedCard.getId() == Card.CARD_ID_J) {

                    if (previousMarble == null) {

                        if (selectedCard.getId() == Card.CARD_ID_7) {
                            // we have a card and marble1 but no split value, the user needs to select a split value
                            drawSplitSevenButtons = true;
                        }

                        // save the marble that was selected
                        previousMarble = marble;

                        screen.draw(getDrawables(screen, gameState).values());
                        return null;

                    }
                    else {
                        marble1 = previousMarble;
                        marble2 = marble;
                    }

                }

                Play userPlay = GameLogic.getPlay(gameState, playerNumber, selectedCard, marble1, marble2, splitSevenValue);
                if (userPlay != null) {
                    return PlayerPlayState.getInstance(playerNumber, userPlay);
                }
                else {
                    // TODO Display an error to the user that the play was not valid?
                    previousMarble = null;
                    splitSevenValue = -1;
                }

            }

        }

        return null;

    }

    @Override
    public State enter(Screen screen, GameState gameState) {

        Preconditions.checkNotNull(gameState, "Null game state");

        // reset the selected card and previous marble
        selectedCard = null;
        previousMarble = null;

        // reset the split 7 logic
        drawSplitSevenButtons = false;
        splitSevenValue = -1;

        // check to see if the game is over
        if (GameLogic.isGameOver(gameState)) {
            return GameOverState.getInstance();
        }

        // check to see if we need to deal a new hand
        if (GameLogic.needToDealNewCards(gameState)) {
            return DealCardsState.getInstance();
        }

        if (screen != null) {

            if (playerNumber != gameState.getUserPlayerNumber()) {
                gameState.setStatus(STATUS_PLAYER_THINKING.replaceFirst(PLAYER_NUMBER_TOKEN, String.valueOf(playerNumber)));
            }
            else {
                gameState.setStatus(STATUS_USER_TURN);
            }

            screen.draw(getDrawables(screen, gameState).values());

        }

        // check to see if there are any valid plays
        Set<Play> plays = GameLogic.getPlays(gameState, playerNumber);
        if (plays.isEmpty()) {

            if (screen != null) {

                /**
                 * Check to see if the player has any cards. If this is the
                 * first time the player cannot play, they will still have there
                 * cards (which will be throw in later in this section of code).
                 * If they do not have their cards, they have already thrown in
                 * their cards so we can skip over them instead of displaying
                 * this message every time we go around the table.
                 */
                if (!gameState.getPlayers().get(playerNumber).getCards().isEmpty()) {

                    // update the status and redraw the screen
                    if (playerNumber != gameState.getUserPlayerNumber()) {
                        gameState.setStatus(STATUS_PLAYER_CANNOT_PLAY.replaceFirst(PLAYER_NUMBER_TOKEN, String.valueOf(playerNumber)));
                    }
                    else {
                        gameState.setStatus(STATUS_USER_CANNOT_PLAY);
                    }

                    screen.draw(getDrawables(screen, gameState).values());

                    try {
                        Thread.sleep(SLEEP_TIME_PLAYER_CANNOT_PLAY);
                    }
                    catch (InterruptedException ex) {
                        Logger.getLogger(PlayerTurnState.class.getName()).log(Level.SEVERE, null, ex);
                    }

                }

            }

            // update the game state to throw in the player's cards
            gameState.getPlayers().get(playerNumber).getCards().clear();

            return PlayerPlayState.getInstance(playerNumber, null);

        }

        if (playerNumber != gameState.getUserPlayerNumber()) {

            if (screen != null) {
                try {
                    Thread.sleep(SLEEP_TIME_PLAYER_THINKING);
                }
                catch (InterruptedException ex) {
                    Logger.getLogger(PlayerTurnState.class.getName()).log(Level.SEVERE, null, ex);
                }
            }

            Play play = GameLogic.getBestPlay(gameState, playerNumber, plays);

            return PlayerPlayState.getInstance(playerNumber, play);

        }

        // return null to stay in this state
        return null;

    }

    @Override
    protected Map<Object, Drawable> getDrawables(Screen screen, GameState gameState) {

        Preconditions.checkNotNull(buttonDrawables, "Null button drawables");

        Map<Object, Drawable> drawables = super.getDrawables(screen, gameState);

        if (selectedCard != null) {
            Drawable drawable = drawables.get(selectedCard);
            if (drawable != null && drawable instanceof CardDrawable) {
                ((CardDrawable) drawable).setSelected(true);
            }
        }

        if (drawSplitSevenButtons) {
            /**
             * TODO Maybe add logic to see if a 7 can be split. If it cannot be
             * split, do not display the buttons and assume it will play as a 7.
             * If it can be split, only enable the buttons that are valid?
             */
            for (ButtonDrawable buttonDrawable : buttonDrawables.keySet()) {
                buttonDrawable.setRectangle(getRectangleForButton(screen, buttonDrawable));
                drawables.put(buttonDrawable, buttonDrawable);
            }
        }

        return drawables;

    }

    @Override
    public State handleInputEvent(Screen screen, GameState gameState, InputEvent inputEvent) {

        Preconditions.checkNotNull(gameState, "Null game state");
        Preconditions.checkNotNull(inputEvent, "Null input event");

        if (playerNumber == gameState.getUserPlayerNumber()) {

            if (inputEvent instanceof TouchEvent) {

                TouchEvent touchEvent = (TouchEvent) inputEvent;
                if (touchEvent.getType() == TouchEvent.TYPE_PRESSED) {
                    return handleTouchEventPressed(screen, gameState, touchEvent);
                }
                else if (touchEvent.getType() == TouchEvent.TYPE_RELEASED) {
                    return handleTouchEventReleased(screen, gameState, touchEvent);
                }

            }

        }

        return null;

    }

    @Override
    public void handleResizeEvent(Screen screen, GameState gameState, int width, int height) {

        // check to see if we have a screen
        if (screen != null) {

            // get the drawables and draw them on the screen
            screen.draw(getDrawables(screen, gameState).values());

        }

    }

}
